<?php
JunctionFileCabinet::using("Junction_Core_Decorator");
JunctionFileCabinet::using("Junction_Core_Iterator");
JunctionFileCabinet::using("Junction_Db_Common_Factory");
JunctionFileCabinet::package("Junction_Clause");
JunctionFileCabinet::package("Junction_Mapping");
JunctionFileCabinet::package("Junction_Query");

/**
 * Represent the user's metadata and facilitate reading 
 * and writing to the database.
 * 
 * <p>This class consists of two classes which it weds 
 * together to fulfill user reads and writes.  A user
 * can request to read a property defined in a mapping.
 * If the property is scalar it is returned otherwise
 * the dependent mapping is loaded and used to generate
 * the dependent objects.  Users can also write scalar
 * properties back to the database.
 * <p>This class is used by the Junction_Core_Base and
 * unsurprisingly does the actual work of mapping between
 * the user's object and the table.  There should never
 * be more than one instance of this class per a mapping
 * file.  Therefore the class's state should be effectively
 * immutable.
 * 
 * @see Junction_Mapping_Table
 * @see Junction_Mapping_References
 */
class Junction_Mapping_Core {
	
	private $_name;
	
	/**
	 * @var Junction_Mapping_Table
	 */
	private $_table;
	
	/**
	 * @var Junction_Mapping_References
	 */
	private $_realtions;
	
	/**
	 * @var Junction_Mapping_References
	 */
	private $_references;
	
	/**
	 * @var Junction_Db_Common_Service
	 */
	private $_dbh;
	
	public function __construct($classname, Junction_Mapping_Table $table, Junction_Mapping_References $refs, Junction_Mapping_References $rels) {
		$this->_name = $classname;
		$this->_table = $table;
		$this->_references = $refs;
		$this->_relations = $rels;
		$this->_dbh = Junction_Db_Common_Factory::construct();
	}
	
	/**
	 * Read the object property's value.
	 * 
	 * <p>Retrieve the property from the record. Note that complex 
	 * properties won't be in the record and must be fetched via 
	 * a dependent mapping object.
	 * <p>Note that when reading complex data types that a database
	 * query will be executed.  Most likely, it is not desirable to
	 * implement some sort of cacheing or similar optimization as 
	 * there is no good way to ensure that the data is fresh.
	 *
	 * @throws Junction_Mapping_Exception
	 * @param String $property
	 * @param Object $object
	 * @return unknown
	 */
	public function read($property, $object) {
		if (!$this->contains($property)) {
			throw new Junction_Mapping_Exception("The requested property is not defined for this mapping.");
		}
		if (!$this->isComplex($property)) {
			return $this->readComplex($property, $object);
		}
		if (!$this->isScalar($property)) {
		 	return Junction_Utils_Reflection_Facade::getValueFor($object, $property);
		}
		return $this->readMany($property, $object);
	}
	
	public function getTable() {
		return $this->_table;
	}
	
	/**
	 * Set the value for the object's property.
	 * 
	 * <p>Note that this will not actually *save* the data but simply
	 * set the data in the record.  In order to save the data
	 * either insert() or update() must be called.
	 * <p>Also note that it is not possible to save complex data
	 * to the database directly.  All complex properties will be
	 * ignored during a save.  In order to save complex types
	 * you must call save on them directly.
	 * 
	 * @throws Junction_Mapping_Exception
	 * @param String $property
	 * @param unknown_type $value
	 * @param Object $object
	 */
	public function write($property, $value, $object) {
		if (!$this->contains($property)) {
			throw new Junction_Mapping_Exception("The requested property is not defined for this mapping."); 
		}
		Junction_Utils_Reflection_Facade::setValueFor($object, $property, $value);
	}
	
	public function getObjectFrom(array $record) {
		$object = Junction_Utils_Reflection_Facade::getInstance($this->_name, array());
		$wrapper = new Junction_Core_Decorator($this->_name, $object);
		foreach ($record as $column => $value) {
			$this->write($this->columnToProperty($column), $value, $object);
		}
		return $wrapper;
	}
	
	// TODO There seems to be some fishy stuff going on with the result sets + iterator
	// The iterator only seems to work correctly when used in a foreach loop...
	public function loadWhere(Junction_Clause_Builder $clause) {
		$query = $this->_table->buildQuery(new Junction_Query_Select(), null);
		$query->bindClause($clause);
		$result = $this->_dbh->select($query->toSql(), $query->getValues());
		return new Junction_Core_Iterator($result, $this);
	}
	
	public function loadMany(Junction_Clause_Builder $clause, $middle) {
		$query = $this->_table->buildQuery(new Junction_Query_Select(), null);
		$query->bindTable($middle);
		$query->bindClause($clause);
		$result = $this->_dbh->select($query->toSql(), $query->getValues());
		return new Junction_Core_Iterator($result, $this);
	}
	
	// currently ignores complex types
	public function insert($object) {
		$query = $this->_table->buildQuery(new Junction_Query_Insert(), $object);
		if (!$this->_dbh->save($query->toSql(), $query->getValues())) {
			return false;
		}
		if (Junction_Utils_Reflection_Facade::getValueFor($object, $this->_table->getPrimaryKey()) == null) {
			Junction_Utils_Reflection_Facade::setValueFor($object, $this->_table->getPrimaryKey(), $this->_dbh->lastInsertId());
		}
		return true;
	}
	
	// currently ignores complex types
	public function update($object) {
		$query = $this->_table->buildQuery(new Junction_Query_Update(), $object);
		
		$clause = new Junction_Clause_Where($this->_table->propertyToColumn($this->_table->getPrimaryKey()) . " = ?");
		$clause->bind(0, Junction_Utils_Reflection_Facade::getValueFor($object, $this->_table->getPrimaryKey()));
		
		$query->bindClause($clause);
		return $this->_dbh->save($query->toSql(), $query->getValues());
	}
	
	public function deleteWhere(Junction_Clause_Builder $clause) {
		$query = $this->_table->buildQuery(new Junction_Query_Delete(), null);
		$query->bindClause($clause);
		if ($this->_dbh->save($query->toSql(), $query->getValues())) {
			return $this->_dbh->affectedRows();
		}
		return 0;
	}
	
	/**
	 * Fetch the value for a dependent class and return it.
	 * 
	 * Load the mapping for the class designated by the property.  
	 * Create a condition object which reflects the condition 
	 * outlined by the user in the metadata.  Bind values using 
	 * data from the record.  Write the resulting iterator over 
	 * mapped objects to the passed object.
	 * 
	 * TODO handle bad case when the join column's don't have 
	 * their values set.
	 *
	 * @param String $property
	 * @param Object $object
	 */
	private function readComplex($property, $object) {
		$complex = $this->propertyToComplex($property); // retrieve an object with a class name, and list of columns
		$mapping = Junction_Core_Factory::construct($complex->getClassname()); // retrieve a mapping for a given class name
		$clause = new Junction_Clause_Conjunction();
		foreach ($complex->getColumns() as $column) { // iterate over join columns
			$clause->bindCondition($column . " = ?", 
				Junction_Utils_Reflection_Facade::getValueFor(
					$object, $this->columnToProperty($column))); // attach a condition to be AND'd
		}
		$this->write($property, $mapping->loadWhere($clause), $object); // execute the query and retrieve an iterator of objects
	}
	
	/**
	 * Fetch the value for a realted class and return it.
	 * 
	 * Load the mapping for the class designated by the property.  
	 * Create a condition object which reflects the condition 
	 * outlined by the user in the metadata.  Bind values using 
	 * data from the record.  Write the resulting iterator over 
	 * mapped objects to the passed object.
	 *
	 * @param String $property
	 * @param Object $object
	 */
	private function readMany($property, $object) {
		$relation = $this->propertyToRelation($property); // retrieve an object with a class name, and list of columns and tables
		$mapping = Junction_Core_Factory::construct($relation->getClassname()); // retrieve a mapping for a given class name
		$clause = new Junction_Clause_Conjunction();
		$middle = $relation->getJoinTable();
		foreach ($relation->getColumns($this->_table) as $column) { // iterate over join columns
			$clause->bindCondition($this->_table . "." . $column . " = ?", 
				Junction_Utils_Reflection_Facade::getValueFor(
					$object, $this->columnToProperty($column))); // attach a condition to be AND'd
		}
		foreach ($relation->getColumns($mapping->getTable()) as $column) { // iterate over join columns
			$clause->bindCondition($mapping->getTable() . "." . $column . " = " .
				$middle . "." . $column); // attach a condition to be AND'd
		}
		$this->write($property, $mapping->loadMany($clause, $middle), $object); // execute the query and retrieve an iterator of objects
	}
	
	/**
	 * Return whether the property is defined for this mapping.
	 *
	 * @param String $property
	 * @return boolean
	 */
	public function contains($property) {
		return ($this->isScalar($property)) ? true : $this->isComplex($property);
	}
	
	/**
	 * Return true if the property is a string, int, etc.
	 *
	 * @param String $property
	 * @return boolean
	 */
	private function isScalar($property) {
		return $this->_table->contains($property);
	}
	
	/**
	 * Return true if the property is a complex reference.
	 * 
	 * @param String $property
	 * @return boolean
	 */
	private function isComplex($property) {
		return $this->_references->contains($property);
	}
	
	/**
	 * Translate the property into a column name.
	 * 
	 * @param String $property
	 * @return String
	 */
	private function propertyToColumn($property) {
		return $this->_table->propertyToColumn($property);
	}
	
	/**
	 * Translate a column back into its property name.
	 * 
	 * @param String $column
	 * @return String
	 */
	private function columnToProperty($column) {
		return $this->_table->columnToProperty($column);
	}
	
	/**
	 * Translate the property into an object with a 
	 * class name and list of columns.
	 * 
	 * @param String $property
	 * @return Junction_Mapping_Association
	 */
	private function propertyToComplex($property) {
		return $this->_references->propertyToComplex($property);
	}
	
	/**
	 * Translate the property into an object with a 
	 * class name, a list of columns and tables.
	 * 
	 * @param String $property
	 * @return Junction_Mapping_Association
	 */
	private function propertyToRelation($property) {
		return $this->_relations->propertyToComplex($property);
	}
}
?>